<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'

const props = defineProps({
  modelValue: {
    type: Object,
    default() {
      return {
        nodesData: [],
        linesData: []
      }
    }
  },
  flowHandleOptions: Array as any,
  flowConditionOptions: Array as any
})

const emit = defineEmits(['update:modelValue'])

//x y为图形左上角坐标
let panelData = ref([
  {
    id: '',
    type: 'start',
    x: 0,
    y: 0,
    width: 120,
    height: 60,
    text: '开始',
    path: 'M 30 0 h 60 a 30 30 0 0 1 30 30 a 30 30 0 0 1 -30 30 h -60 a 30 30 0 0 1 -30 -30 a 30 30 0 0 1 30 -30',
    fill: '#fff',
    stroke: '#123456'
  },
  {
    id: '',
    type: 'handle',
    x: 0,
    y: 0,
    width: 120,
    height: 60,
    text: '处理',
    path: 'M 5 0 h 110 a 5 5 0 0 1 5 5 v 50 a 5 5 0 0 1 -5 5 h -110 a 5 5 0 0 1 -5 -5 v -50 a 5 5 0 0 1 5 -5',
    fill: '#fff',
    stroke: '#123456',
    properties: {
      label: '',
      value: ''
    }
  },
  {
    id: '',
    type: 'condition',
    x: 0,
    y: 0,
    width: 120,
    height: 80,
    text: '条件',
    path: 'M 60 0 L 120 40 L 60 80 L 0 40 z',
    fill: '#fff',
    stroke: '#123456',
    properties: {
      field: '',
      label: '',
      type: '',
      comparison: '',
      value: ''
    }
  },
  {
    id: '',
    type: 'end',
    x: 0,
    y: 0,
    width: 120,
    height: 60,
    text: '结束',
    path: 'M 30 0 h 60 a 30 30 0 0 1 30 30 a 30 30 0 0 1 -30 30 h -60 a 30 30 0 0 1 -30 -30 a 30 30 0 0 1 30 -30',
    fill: '#fff',
    stroke: '#123456'
  }
])
//操作的连线
let activeLine = ref({
  id: '',
  type: 'line',
  fromId: '',
  toId: '',
  startPoint: { x: 0, y: 0 },
  endPoint: { x: 0, y: 0 },
  points: '',
  path: '',
  textCoord: { x: 0, y: 0 },
  text: ''
})

const resetActiveLine = () => {
  activeLine.value = {
    id: '',
    type: 'line',
    fromId: '',
    toId: '',
    startPoint: { x: 0, y: 0 },
    endPoint: { x: 0, y: 0 },
    points: '',
    path: '',
    textCoord: { x: 0, y: 0 },
    text: ''
  }
}

let flowData = computed(() => props.modelValue)

//watchEffect数据响应有问题用watch
watch(flowData, (cur) => emit('update:modelValue', cur), { deep: true })

let activeNode = ref<any>({ x: 0, y: 0 })
let resetActiveNode = () => {
  activeNode.value = { x: 0, y: 0 }
}

let bodyNode = ref({
  x: 0,
  y: 0,
  scale: 1,
  transformOrigin: [0, 0]
})

let grabbing = ref(false)
//拖拽事件
//添加
const dragstartCopy = (ev, type) => {
  origin.value.x = ev.screenX
  origin.value.y = ev.screenY

  let copyData = JSON.parse(JSON.stringify(panelData.value.find((v) => v.type == type)))
  copyData.id = Math.random().toString(16).substring(2)
  //格式化10px的步长 减去svg body的偏差
  //class panel的top、left需为0时当前事件offset和class draw的相同
  copyData.x = Math.round(ev.offsetX / 10) * 10 - bodyNode.value.x
  copyData.y = Math.round(ev.offsetY / 10) * 10 - bodyNode.value.y
  flowData.value.nodesData.push(copyData)
  activeNode.value = copyData
}
//移动
//上次移动事件的坐标
let origin = ref({ x: 0, y: 0 })

const dragstartMove = (ev) => {
  grabbing.value = true

  origin.value.x = ev.screenX
  origin.value.y = ev.screenY

  if (ev.target.dataset.id === 'body') activeNode.value = bodyNode.value
  else {
    activeNode.value =
      flowData.value.nodesData.find((v) => v.id === ev.currentTarget.dataset.id) ?? activeNode.value
  }
}

const dragover = (ev) => {
  let movementX = ev.screenX - origin.value.x
  let movementY = ev.screenY - origin.value.y
  if (Math.abs(movementX) >= 10) {
    activeNode.value.x += parseInt(movementX / 10) * 10
    origin.value.x = ev.screenX - (movementX % 10)
  }
  if (Math.abs(movementY) >= 10) {
    activeNode.value.y += parseInt(movementY / 10) * 10
    origin.value.y = ev.screenY - (movementY % 10)
  }

  flowData.value.linesData.forEach((v) => makeLinePath(v))

  setDragLineTo(ev)
}

const dragend = () => {
  resetActiveNode()
  resetBufferLine()
  popoverMoveable.value = false
}
// 缩放
const onWheel = (e) => {
  e.preventDefault()
  bodyNode.value.transformOrigin = [e.offsetX, e.offsetY]
  bodyNode.value.scale -= e.deltaY / 1000
}
// 生成曲线路径
function generateBezierCurves(start, end, direction) {
  const { x: startX, y: startY } = start
  const { x: endX, y: endY } = end

  const offsetX = endX - startX
  const offsetY = endY - startY

  if (direction == 'horizontal') {
    return `M${startX} ${startY} C ${startX + offsetX / 2} ${startY}, ${endX - offsetX / 2} ${endY}, ${endX} ${endY}`
  } else {
    return `M${startX} ${startY} C ${startX} ${startY + offsetY / 2}, ${endX} ${endY - offsetY / 2}, ${endX} ${endY}`
  }
}
//锚点生成连线
const makeLinePath = (line) => {
  let fromNode = flowData.value.nodesData.find((v) => v.id === line.fromId)
  let toNode = flowData.value.nodesData.find((v) => v.id === line.toId)

  if (fromNode === toNode) return false

  if (fromNode && toNode) {
    let startPointX = fromNode.x + fromNode.width / 2
    let startPointY = fromNode.y + fromNode.height / 2
    let endCenterX = toNode.x + toNode.width / 2
    let endCenterY = toNode.y + toNode.height / 2
    let endPointX = endCenterX
    let endPointY = endCenterY
    let midPoint1X = startPointX
    let midPoint1Y = startPointY
    let midPoint2X = startPointX
    let midPoint2Y = endCenterY

    // 曲线
    const offsetX = endCenterX - startPointX
    const offsetY = endCenterY - startPointY
    let direction = ''
    const markerWidth = 10

    if (Math.abs(offsetX) > Math.abs(offsetY)) direction = 'horizontal'
    else direction = 'vertical'

    line.startPoint = { x: startPointX, y: startPointY }

    if (direction == 'horizontal') {
      line.endPoint = {
        x: endPointX + Math.sign(startPointX - endPointX) * (toNode.width / 2 + markerWidth),
        y: endPointY
      }
    } else {
      line.endPoint = {
        x: endPointX,
        y: endPointY + Math.sign(startPointY - endPointY) * (toNode.height / 2 + markerWidth)
      }
    }

    line.path = generateBezierCurves(line.startPoint, line.endPoint, direction)
    // 折线
    //判断首尾直线连线是否穿过节点
    let collision: any = false
    for (let node of flowData.value.nodesData) {
      if (
        startPointX >= node.x &&
        startPointX <= node.x + node.width &&
        startPointY <= node.y &&
        endPointY >= node.y + node.height
      ) {
        collision = 'vertical'
        break
      }
      if (
        startPointY >= node.y &&
        startPointY <= node.y + node.height &&
        startPointX <= node.x &&
        endPointX >= node.x + node.width
      ) {
        collision = 'horizontal'
        break
      }
    }

    if (!collision) {
      //无碰撞情况
      if (midPoint2X < endPointX && midPoint2X - endPointX < -(toNode.width / 2 + 10)) {
        endPointX = endPointX - toNode.width / 2 - 10
      } else if (midPoint2X > endPointX && midPoint2X - endPointX > toNode.width / 2 + 10) {
        endPointX = endPointX + toNode.width / 2 + 10
      } else {
        if (startPointY < endCenterY) endPointY = endCenterY - toNode.height / 2 - 10
        else endPointY = endCenterY + toNode.height / 2 + 10
        midPoint2Y = midPoint1Y = (startPointY + endPointY) / 2
        endPointX = startPointX
      }
    } else {
      if (collision == 'vertical') {
        midPoint2X = midPoint1X = midPoint1X + Math.abs(startPointY) / 4 + 100 //至少100px的间隔

        if (midPoint2X < endPointX && midPoint2X - endPointX < -(toNode.width / 2 + 10)) {
          endPointX -= toNode.width / 2 + 10
        } else if (midPoint2X > endPointX && midPoint2X - endPointX > toNode.width / 2 + 10) {
          endPointX += toNode.width / 2 + 10
        } else {
          if (startPointY < endCenterY) endPointY = endCenterY - toNode.height / 2 - 10
          else endPointY = endCenterY + toNode.height / 2 + 10
          midPoint2Y = midPoint1Y
          endPointX = midPoint2X
        }
      }
      if (collision == 'horizontal') {
        midPoint2X = endPointX
        midPoint2Y = midPoint1Y = midPoint1Y + Math.abs(startPointX) / 4 + 100

        if (midPoint2Y < endPointY && midPoint2Y - endPointY < -(toNode.height / 2 + 10)) {
          endPointY -= toNode.height / 2 + 10
        } else if (midPoint2Y > endPointY && midPoint2Y - endPointY > toNode.height / 2 + 10) {
          endPointY += toNode.height / 2 + 10
        } else {
          if (startPointX < endCenterX) endPointX = endCenterX - toNode.width / 2 - 10
          else endPointX = endCenterX + toNode.width / 2 + 10
          midPoint2X = midPoint1X
          endPointY = midPoint2Y
        }
      }
    }

    let points = `${startPointX} ${startPointY},
    ${midPoint1X} ${midPoint1Y},
    ${midPoint2X} ${midPoint2Y},
    ${endPointX} ${endPointY}`

    line.points = points
    line.textCoord = { x: (midPoint2X + endPointX) / 2, y: (midPoint2Y + endPointY) / 2 }

    //同一对node一个方向只能连一次
    if (!flowData.value.linesData.find((v) => v.fromId == line.fromId && v.toId == line.toId)) {
      line.id = Math.random().toString(16).substring(2)

      flowData.value.linesData.push(line)
    }
  }
}

const setLineFrom = (id) => {
  let fromNode = flowData.value.nodesData.find((v) => v.id == id)
  setDragLineFrom(fromNode)

  activeLine.value.fromId = id
}
const setLineTo = (id) => {
  activeLine.value.toId = id
  makeLinePath(activeLine.value)

  resetBufferLine()
  resetActiveNode()
}

//操作的虚线
let dragLineRef = ref(null)
let dragLine = ref({
  fromPoint: { x: 0, y: 0 },
  toPoint: { x: 0, y: 0 }
})
const setDragLineFrom = (fromNode) => {
  dragLine.value.fromPoint.x = dragLine.value.toPoint.x = fromNode.x + fromNode.width / 2
  dragLine.value.fromPoint.y = dragLine.value.toPoint.y = fromNode.y + fromNode.height / 2
  dragLineRef.value.style.display = 'block'
}

const setDragLineTo = (ev) => {
  dragLine.value.toPoint.x = ev.offsetX - bodyNode.value.x
  dragLine.value.toPoint.y = ev.offsetY - bodyNode.value.y
}
const resetBufferLine = () => {
  dragLineRef.value.style.display = 'none'
  dragLine.value = {
    fromPoint: { x: 0, y: 0 },
    toPoint: { x: 0, y: 0 }
  }

  resetActiveLine()
}

//右键菜单
let contextmenuRef = ref(null)
let dataOfOpen = ref(null) // menu的data
let popoverData: any = ref({})
let popoverRef = ref()
let popoverMoveable = ref(false)
let popoverOffset = ref({ x: 0, y: 0 })

const openMenu = (ev, type) => {
  if (type === 'node') {
    contextmenuRef.value.show()
    dataOfOpen.value = flowData.value.nodesData.find((v) => v.id === ev.currentTarget.dataset.id)
    popoverData.value = dataOfOpen.value
  }
  if (type === 'line') {
    contextmenuRef.value.show()
    dataOfOpen.value = flowData.value.linesData.find((v) => v.id === ev.currentTarget.dataset.id)
    popoverData.value = dataOfOpen.value
  }
}
const moveContextmenu = (ev) => {
  contextmenuRef.value.style.left = `${ev.offsetX}px`
  contextmenuRef.value.style.top = `${ev.offsetY}px`
}
const closeMenu = () => {
  dataOfOpen.value = {}
  contextmenuRef.value.close()
}
const remove = () => {
  flowData.value.nodesData = flowData.value.nodesData.filter((v) => v.id !== dataOfOpen.value.id)
  flowData.value.linesData = flowData.value.linesData.filter(
    (v) =>
      v.id !== dataOfOpen.value.id &&
      v.fromId !== dataOfOpen.value.id &&
      v.toId !== dataOfOpen.value.id
  )
}
const closePopover = () => {
  popoverData.value = {}
  popoverRef.value.hidePopover()
}

const openpopover = () => {
  nextTick(() => {
    const rect = contextmenuRef.value.getBoundingClientRect()

    popoverRef.value.style.left = rect.left + 'px'
    popoverRef.value.style.top = rect.top + 'px'
  })
}

const popoverMoveStart = (ev) => {
  popoverMoveable.value = true
  popoverOffset.value.x = ev.pageX - popoverRef.value.offsetLeft
  popoverOffset.value.y = ev.pageY - popoverRef.value.offsetTop
}
const popoverMove = (ev) => {
  if (popoverMoveable.value) {
    popoverRef.value.style.left = ev.pageX - popoverOffset.value.x + 'px'
    popoverRef.value.style.top = ev.pageY - popoverOffset.value.y + 'px'
  }
}
</script>

<template>
  <div
    class="flow-container"
    @pointerup="dragend()"
    @pointerleave="dragend()"
    @contextmenu.prevent="moveContextmenu"
    @pointerdown="closeMenu"
    @pointermove="popoverMove($event)"
    @wheel="onWheel"
  >
    <svg class="background" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
      <defs>
        <pattern id="svg-bg-grid" patternUnits="userSpaceOnUse" x="0" y="0" width="10" height="10">
          <path d="M 10 0 H0 M0 0 V0 10" stroke="#DCDCDC" stroke-width="1" opacity="1" />
        </pattern>
      </defs>
      <g>
        <rect width="100%" height="100%" fill="url(#svg-bg-grid)" />
      </g>
    </svg>
    <svg
      class="panel"
      width="90"
      :height="panelData.length * 55"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g
        v-for="(item, index) of panelData"
        :key="item.type"
        class="cursor-pointer"
        :transform="`translate(9, ${55 * index + 10}) scale(.6)`"
        @pointerdown="dragstartCopy($event, item.type)"
      >
        <path :d="item.path" :stroke="item.stroke" :fill="item.fill" stroke-width="2" />
        <text
          :x="item.width / 2"
          :y="item.height / 2"
          font-size="25"
          text-anchor="middle"
          dominant-baseline="central"
          user-select="none"
        >
          {{ item.text }}
        </text>
      </g>
    </svg>
    <svg
      class="draw"
      :class="{ 'cursor-grabbing': grabbing }"
      xmlns="http://www.w3.org/2000/svg"
      width="100%"
      height="100%"
      data-id="body"
      @pointerdown="dragstartMove($event)"
      @pointermove="dragover($event)"
      @pointerup="grabbing = false"
    >
      <defs>
        <marker
          id="svg-line-triangle"
          viewBox="0 0 10 10"
          refX="1"
          refY="5"
          markerUnits="userSpaceOnUse"
          markerWidth="10"
          markerHeight="10"
          orient="auto"
        >
          <path d="M 0 0 L 10 5 L 0 10 z" fill="#000" />
        </marker>
      </defs>

      <g
        :transform-origin="bodyNode.transformOrigin.join(' ')"
        :transform="`translate(${bodyNode.x}, ${bodyNode.y}) scale(${bodyNode.scale})`"
      >
        <g
          v-for="item of flowData.linesData"
          :key="item.id"
          class="line cursor-auto"
          :data-id="item.id"
          @contextmenu="openMenu($event, 'line')"
        >
          <!-- 宽线点击用 -->
          <!-- <polyline fill="none" stroke="transparent" :points="item.points" stroke-width="30" />
          <polyline
            fill="none"
            stroke="black"
            :points="item.points"
            stroke-width="1"
            marker-end="url(#svg-line-triangle)"
          />
          <text :x="item.textCoord.x" :y="item.textCoord.y" text-anchor="middle">
            {{ item.text }}
          </text> -->
          <!-- 宽线点击用 -->
          <path
            display="none"
            class="active-line"
            :d="item.path"
            fill="none"
            stroke="rgba(0, 0, 0, 0.1)"
            stroke-width="30"
          ></path>
          <path :d="item.path" fill="none" stroke="transparent" stroke-width="30"></path>
          <path
            :id="`line-${item.id}`"
            :d="item.path"
            fill="none"
            stroke="black"
            stroke-width="1"
            marker-end="url(#svg-line-triangle)"
          ></path>
          <text>
            <textPath :href="`#line-${item.id}`">
              <tspan x="100">{{ item.text }}</tspan>
            </textPath>
          </text>
        </g>

        <g
          v-for="item of flowData.nodesData"
          :key="item.id"
          class="node cursor-move"
          style="filter: drop-shadow(0 0 12px rgba(0, 0, 100, 0.12))"
          :data-id="item.id"
          :transform="`translate(${item.x}, ${item.y})`"
          @pointerdown.stop="dragstartMove($event)"
          @contextmenu="openMenu($event, 'node')"
        >
          <path
            :d="item.path"
            :stroke="item.stroke"
            :fill="item.fill"
            stroke-opacity="0.2"
            stroke-width="3"
          />
          <path
            class="active-node"
            display="none"
            :d="item.path"
            stroke="rgba(100, 255, 255, 1)"
            :fill="item.fill"
            stroke-width="2"
          />
          <text
            :x="item.width / 2"
            :y="item.height / 2"
            text-anchor="middle"
            dominant-baseline="central"
          >
            {{ item.text }}
          </text>
          <text
            v-if="item.type == 'handle'"
            :x="item.width / 2"
            :y="item.height / 2 + 25"
            text-anchor="middle"
          >
            {{ item.properties.label }}
          </text>
          <text
            v-if="item.type == 'condition'"
            :x="item.width / 2"
            :y="item.height / 2 + 25"
            text-anchor="middle"
            fill="#000"
            stroke-width="0.1"
            stroke="#fff"
          >
            {{ item.properties.label + item.properties.comparison + item.properties.value }}
          </text>
          <circle
            class="anchor cursor-pointer"
            :cx="item.width / 2"
            :cy="item.height / 2"
            r="6"
            fill="#fff"
            stroke="#000"
            stroke-width="3"
            opacity="0.5"
            display="none"
            @pointerdown.stop="setLineFrom(item.id)"
            @pointerup.stop="setLineTo(item.id)"
          />
        </g>
        <line
          ref="dragLineRef"
          :x1="dragLine.fromPoint.x"
          :y1="dragLine.fromPoint.y"
          :x2="dragLine.toPoint.x"
          :y2="dragLine.toPoint.y"
          stroke-dasharray="3 2"
          stroke="#000"
          display="none"
          pointer-events="none"
        />
      </g>
    </svg>

    <dialog ref="contextmenuRef" class="menu">
      <!-- stop阻止冒泡，不然popover不能正常显示 -->
      <button
        class="menu-item"
        popovertarget="flow-edit-popover"
        popovertargetaction="show"
        @pointerdown.stop="openpopover"
      >
        编辑
      </button>
      <button class="menu-item" @pointerdown="remove">删除</button>
    </dialog>

    <div id="flow-edit-popover" ref="popoverRef" popover="auto" @contextmenu.stop.prevent="">
      <div class="close-popover cursor-pointer" @pointerdown.stop="closePopover">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 1024 1024"
          style="width: 100%; height: 100%"
        >
          <path d="M 0 0 L 1000 1000 M 1000 0 L 0 1000" stroke-width="100" stroke="#123456" />
        </svg>
      </div>
      <div class="popover-drag cursor-move" @pointerdown.stop="popoverMoveStart($event)">
        <strong>编辑</strong>
      </div>
      <div>
        <span class="popover-span">节点文本</span>
        <span>：</span><input v-model="popoverData.text" />
      </div>
      <div>
        <div v-if="popoverData.type == 'handle'">
          <span class="popover-span">执行人</span><span>：</span>
          <select v-model="popoverData.properties">
            <option v-for="item of flowHandleOptions" :key="item.value" :label="item.label">
              {{ item.label }}
            </option>
          </select>
        </div>
        <div v-if="popoverData.type == 'condition'">
          <span class="popover-span">条件</span><span>：</span>
          <select
            v-model="popoverData.properties.field"
            @change="
              ($event: any) =>
                Object.assign(
                  popoverData.properties,
                  flowConditionOptions.find((v) => v.field == $event.target.value)
                )
            "
          >
            <option disabled value="">表单项</option>
            <option v-for="opt of flowConditionOptions" :key="opt.filed" :value="opt.field">
              {{ opt.label }}
            </option>
          </select>
          <select v-model="popoverData.properties.comparison" style="width: 58px">
            <option disabled value="">比较</option>
            <option v-for="opt of ['小于', '等于', '大于']" :key="opt" :value="opt">
              {{ opt }}
            </option>
          </select>
          <input
            v-if="popoverData.properties.type == 'number'"
            v-model="popoverData.properties.value"
            type="number"
          />
          <select
            v-if="popoverData.properties.type == 'radio'"
            v-model="popoverData.properties.value"
          >
            <option disabled value="">选项</option>
            <option v-for="opt of popoverData.properties.options" :key="opt.value">
              {{ opt.label }}
            </option>
          </select>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
* {
  user-select: none;
}

.flow-container {
  width: 100%;
  height: 700px;
  position: relative;
}
.background {
  position: absolute;
  z-index: 0;
}
.panel {
  border-radius: 5px;
  background-color: #fff;
  opacity: 0.7;
  box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 10px 1px;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 20;
}
.draw {
  cursor: grab;
  position: absolute;
  z-index: 10;
}

.menu {
  font-size: 14px;
  width: 70px;
  padding: 8px;
  margin: 0;
  border: none;
  background-color: #fff;
  box-shadow: 0 0 12px rgba(0, 0, 0, 0.12);
  border-radius: 5px;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 30;
}
.menu-item {
  cursor: pointer;
  width: 100%;
  border: none;
  background-color: #fff;
}
.menu-item:hover {
  background-color: #eee;
  user-select: none;
}
.node:hover .anchor {
  display: block;
}
.node:hover .active-node {
  display: block;
}
.line:hover .active-line {
  display: block;
}
#flow-edit-popover {
  font-size: 14px;
  padding: 5px 10px 20px;
  margin: 0;
  border: none;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 12px rgba(0, 0, 0, 0.12);
  z-index: 40;
}
.popover-drag {
  height: 20px;
  border-bottom: 1px solid #abc;
}
.close-popover {
  width: 13px;
  height: 13px;
  position: absolute;
  right: 15px;
  top: 5px;
}
.popover-span {
  display: inline-block;
  width: 60px;
  text-align-last: justify;
}
[popover] input,
[popover] select {
  width: 100px;
  margin-top: 10px;
  padding: 3px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  outline: none;
}
.cursor-auto {
  cursor: auto;
}
.cursor-pointer {
  cursor: pointer;
}
.cursor-move {
  cursor: move;
}
.cursor-grabbing {
  cursor: grabbing;
}
</style>
